########################################################################
#  Searx-qt - Lightweight desktop application for SearX.
#  Copyright (C) 2020  CYBERDEViL
#
#  This file is part of Searx-qt.
#
#  Searx-qt is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  Searx-qt is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
########################################################################


from searxqt.core import log


class ValueBase:
    """ Base for evaluatable data type(s).
    """
    def evaluate(self, valueType):
        return False


class Value(ValueBase):
    """ Evaluatable data type.
    """
    def __init__(self, dataType):
        """
        @param dataType: The data type this should match.
        @type dataType: type
        """
        self.__dataType = dataType

    def __repr__(self):
        return str(self)

    def __str__(self):
        return str(self.__dataType)

    def evaluate(self, valueType):
        if valueType is self.__dataType:
            return True
        return False


class MultiValue(ValueBase):
    """ For when multiple data types may be valid.
    """
    def __init__(self, dataTypes):
        """
        @param dataTypes: A tuple with one or more acceptable data types.
        @type dataTypes: tuple(type, ..)
        """
        self.__dataTypes = dataTypes

    def __repr__(self):
        return str(self)

    def __str__(self):
        return "({0})".format(
            ", ".join([str(dataType) for dataType in self.__dataTypes])
        )

    def evaluate(self, valueType):
        if valueType in self.__dataTypes:
            return True
        return False


class IgnoreValue:
    """ Use a instance of this class to ignore a value.
    """
    pass


NoneType = type(None)


def verifyStructure(structure, data, path="root"):
    """
     - Verify data types of defined data structure.
     - Warn on unknown keys in dict.

    @param structure: Expected data structure
    @type structure: any

    @param data: Data to compare with the expected structure.
    @type data: any

    @param path: This is only used to keep track of where a issue occured.
    @type path: str

    @return: Verification status and error message.
    @rtype: tuple(bool, str)
    """

    dataType = type(data)
    structureType = type(structure)

    # Verify if the current data has the expected type.

    if isinstance(structure, ValueBase):
        # Multiple types may be valid.
        if not structure.evaluate(dataType):
            error = (
                "Mismatched! (1) Expected a `{0}` but got `{1}` for {2}"
            ).format(str(structure), str(dataType), path)
            return (False, error)

    elif structureType is IgnoreValue:
        # Struct says ignore this check.
        return (True, "")

    elif dataType != structureType:
        # Data value isn't of expected type.
        error = (
            "Mismatched! (2) Expected a `{0}` but got `{1}` for {2}"
        ).format(structureType, dataType, path)
        return (False, error)

    # Recurse iterable objects.
    if dataType is dict:
        if not data:
            # Empty data dict, nothing to evaluate.
            return (True, "")
        elif not structure:
            # Empty dict structure; skip check.
            return (True, "")

        if "" in structure:
            # The key is variabble
            for key in data:
                verified, error = verifyStructure(
                    structure[""],
                    data[key],
                    path="{0}['{1}']".format(path, key)
                )
                if not verified:
                    return (False, error)
        else:
            # Constant key
            for key in data:
                if key not in structure:
                    log.warning("Unknown key", key, type(data[key]), path)
                    continue

                verified, error = verifyStructure(
                    structure[key],
                    data[key],
                    path="{0}['{1}']".format(path, key)
                )
                if not verified:
                    return (False, error)

    elif dataType is list:
        if not data:
            # Empty data list, nothing to evaluate.
            return (True, "")
        elif not structure:
            # Empty list structure; skip check.
            return (True, "")

        structLen = len(structure)

        if structLen == 1:
            # Repeated match
            index = 0
            for value in data:
                verified, error = verifyStructure(
                    structure[0],
                    value,
                    path="{0}['{1}']".format(path, index)
                )
                if not verified:
                    return (False, error)
                index += 1

        elif structLen != len(data):
            # Mismatch!
            return (
                False,
                (
                    "Expected a fixed length of {0} elements but got a length"
                    " of {1} elements instead for path: {2}"
                ).format(structLen, len(data), path)
            )

        else:
            # Structure within list (fixed length)
            index = 0
            for value in structure:
                verified, error = verifyStructure(
                    value,
                    data[index],
                    path="{0}['{1}']".format(path, index)
                )
                if not verified:
                    return (False, error)
                index += 1

    return (True, "")
